SexLab VR 原生动画对象修复 v0.3.3
（基于 v0.2.2 基线重做定位逻辑，替代 v0.2.4 ~ v0.2.14）


这一版为什么要推翻 v0.2.14
==========================
v0.2.4 到 v0.2.14 走的是"由 DLL 自己算出家具应该摆在世界的哪个位置"这条路，
每发现一种错位就加一层补偿。到 v0.2.14 时，这套补偿本身成了错位的主要来源。

经过对 v0.2.2 与 v0.2.14 源码的逐行比对，确认了四个问题：

1) 分类过宽（这是"新版比旧版错位更多"的直接原因）
   v0.2.14 的 FindAttachNode 里：

       const bool isGroundObject =
           (locator == "Frn" && normalizedNode == "scene root") ||
           normalizedNode == "animobjecta" ||
           normalizedNode == "animobjectb" ||
           normalizedNode == "npc root [root]" ||
           normalizedNode == "pa groundobjectr";

   而 nodeName 的初始值就是 "NPC Root [Root]"。也就是说，凡是 NIF 上
   没有写 Prn/Frn 的 AnimObject，全部被判定成"地面家具"，被搬到 cell
   节点上用手算坐标摆放。

   但 AnimObjectA / AnimObjectB 是 Skyrim 骨架里的标准挂点，动画文件
   本身就带这两个骨骼的关键帧轨道——家具该在哪，动画数据已经写好了。
   把它们改成"世界固定"，等于把动画对家具的驱动整个丢掉，再用一套
   手算变换去凑。这必然大面积错位。

2) 跨场景污染
   _groundAnchorXY 以 editorID 为键缓存锚点，永不清理，跨场景、跨存档
   一直复用。换个地点做同一个动画，用的还是上一次的锚点。

3) 拿错角色的骨架做参考（对应"该在 A 背后的跑到 B 身上"）
   referenceAnchor 是从"另一个参与者"（NPC）的骨架上读 AnimObjectA/B
   偏移，再套到玩家的槽位坐标上。但那个 NPC 正在播的是 A2/A3 动画，
   它的 AnimObject 节点偏移和玩家 A1 动画的偏移没有对应关系。

4) 缩放被乘了两次（对应"板凳歪半边、床歪半边"）
   objectWorld = slotWorld * referenceAnchor * nifLocal
   NiTransform 连乘会把 referenceAnchor 的位移再乘一次 PlayerScale，
   而 referenceAnchor 本身已经是骨架空间的量。

以上四点，v0.2.2 一条都没有——它们全是 v0.2.4 之后加进去的。


v0.3.0 的做法
=============
回到引擎原生语义，只在真正必要的地方做修正。

* 分类：v0.2.2 行为 + VR 修正（v0.3.2 校准）
  v0.2.2 的原始做法是：除了显式 `Frn="Scene Root"`，其他全部按引擎原生方式
  挂在对应骨骼上。AnimObjectA / AnimObjectB 是 Skyrim 骨架里"由动画驱动
  的虚拟骨骼"——身上的链条、项圈、剑、魔法效果、绑着的手等等，全部靠这两根
  骨骼来定位。NIF 自身偏移也是相对它们写好的，动画文件带这两根骨骼的关键帧。
  因此在 v0.3.3 它们继续走 bone-attached，跟随身体。

  VR 里需要修正的是这几类，改挂 cell 场景图（世界固定）：

    - NIF 写了 Scene Root（Prn 或 Frn 都算）
    - 没有 Prn/Frn 时，回退到默认 NPC Root [Root] 的家具
      （刑架、柱、十字架、枷锁架——这些原本就在 v0.2.2 里因为 NPC Root 旋转
       而飘动）
    - "PA GroundObject*" Prn 前缀（Papyrus 驱动的地面锚点；v0.2.2 靠
      NPC Root 凑巧在脚底所以看着在手上，VR 里 NPC Root 跟 HMD 走就
      漂到半空；改世界固定更接近原意）
    - Frn="AnimObjectA/B"（v0.3.3 新增）—— SexLab 规范：Prn 是主角色
      虚拟骨骼（玩家身上佩戴的道具），Frn 是参考角色虚拟骨骼。
      `Prn='AnimObject*'` 是身上的道具（链条/剑/项圈/绑手/魔法），
      必须跟随身体；`Frn='AnimObject*'` 是钉在 NPC 虚拟骨骼上的
      场景家具（AnimationsByLeito 的 X 架/颈手枷等），VR 里 NPC
      骨骼也不稳定，统一改世界固定。

  其他所有骨骼（含 AnimObjectA / AnimObjectB 和具体肢体骨骼）一律走
  引擎原生路径，不做变换。

* 骨架挂载路径不做任何变换
  动画驱动节点，NIF 的局部变换本来就是相对该节点写好的。这条路径上
  刻意不碰 local——任何"修正"都是错位的来源。

* 世界固定路径保留 v0.2.5 唯一正确的那一条
  确实需要世界固定的家具，挂到 cell 场景图而不是玩家 3D 根，这样不会
  继承 VRIK/头显带来的旋转（解决"随镜头飘动"）。
  定位只用 SexLab Loc（水平投影，只保留朝向）+ NIF 自身局部变换。
  不再使用 NPC 参考骨架，不再有永久缓存。

* 保留 v0.2.7 的可见性刷新
  重挂后递归解除隐藏并刷新世界变换与包围体，这是修复"隐形"的关键，保留。

* 事件冲突会告警
  两个动画包定义了同一个动画事件时，双方的 AnimObject 会被合并挂到同
  一个人身上。这也是"家具跑到别人身上"的一种成因，现在会在日志里明确
  写出是哪两个 FNIS 列表冲突。


安装
====
1. 在 MO2 中停用 v0.2.4 ~ v0.2.14 以及之前的 v0.3.0 / v0.3.1 / v0.3.2。
   本版是完整替代，不要叠加。
2. 将本压缩包作为一个新 MOD 安装，放在 SexLab VR Patch 和相关 hotfix 后面。
3. 确认本 MOD 的 scripts\sslActorAlias.pex 赢得文件冲突。
4. 通过 SKSEVR 启动游戏。

脚本说明：本版沿用 v0.2.14 的 Papyrus 脚本，未作改动。DLL 侧保留了
ApplyForEventAt 的原有五参数签名，因此脚本无需重新编译；第五个参数
（参考角色）仍会被接收，但按设计不再参与定位。


如果还有个别家具错位：不用等重编译
==================================
新增了一个可选的覆盖文件，游戏启动时读取，可以手动指定某个 AnimObject
用哪种方式挂载。文件位置：

    Data\SKSE\Plugins\SLVRAnimObjectNative.ini

写法（等号左边是 ANIO 的 EditorID，日志里能查到）：

    [Overrides]
    AOXCrossFrame = world     ; 强制钉在地面/世界
    SomeHandProp  = bone      ; 强制跟随骨架

取值 world / ground / fixed / scene 等价；bone / skeleton / actor / follow 等价。
不需要这个文件时可以不放，压缩包里给的是模板，默认所有条目都被注释掉了。

v0.3.3 已经把 AOXCross（X 架）和 AOPillory（颈手枷）从 bone 自动改为 world
（依据是它们用 Frn='AnimObjectB' 标记——是 mod 作者把场景家具挂在 NPC 虚拟
骨骼上）。如果你用其他动画包还遇到类似情况，告诉我 EditorID 我直接写进默认
逻辑里，或者你自己加进 ini 即可。

判断该填哪个：
- 家具跟着你的头/镜头飘  -> 填 world
- 家具钉死在地上但应该跟着身体动（比如挂在身上的道具）-> 填 bone


日志
====
Documents\My Games\Skyrim VR\SKSE\SLVRAnimObjectNative.log
（MO2 用户通常在 overwrite\SKSE\ 下）

正常启动应看到：
- SLVRAnimObjectNative 0.3.3 loaded
- Indexed ... FNIS lists, ... animation events, ... AnimObject references

每次挂载会记录一行，形如：
- Bone-attached  <EditorID> (<nif>) via Prn='AnimObjectA' -> node '...' world=(x,y,z)
- World-fixed    <EditorID> (<nif>) via Frn='Scene Root' source=SexLabLoc parent='...' world=(x,y,z)

反馈时请附上日志，并说明哪个动画对应哪种错位（歪 / 飘 / 跑到别人身上），
日志里的 EditorID 可以直接填进上面的 ini。


构建
====
源码在 source\ 下，与 v0.2.14 相同的 xmake 工程结构：

    <父目录>/
      CommonLibSSE-NG-main/          CommonLibSSE-NG (main 分支)
      deps/spdlog-1.15.3/
      deps/rapidcsv-master/
      SLVRAnimObjectNative/          本工程

    xmake f -p windows -a x64 -m release
    xmake build

需要 MSVC (C++23) 与 Windows SDK。


验证范围
========
DLL 已在 x64 Release 下成功编译，导出 SKSEPlugin_Load / Query / Version。
Papyrus 脚本沿用 v0.2.14 已编译产物，未改动。
尚未进行游戏内运行验证，仍属实验测试版。
